﻿/**
 * BeatGame engine tem como objetivo ser uma engine para um jogo "GUITAR HERO like", 
 * sendo o mais generica e configuravel possivel, para que possa ser utilizada em qualquer projeto
 * futurameente.
 */
// TODO : criar controles de excessão e evitar que erros saiam da engine sem serem tratados
package org.leles.engines.beatgame 
{
	import flash.events.TimerEvent;
	import flash.system.System;
	import flash.utils.Timer;
	import org.leles.common.ITaggedObject;
	import org.leles.engines.beatgame.BeatGameTrack;
	/**
	 * ...
	 * @author Bruno leles
	 * @version 0.3 alpha
	 */
	public class BeatGameEngine extends BeatGameObject
	{
		public static const R_SONG_COMPLETED:String				= "R_SONG_COMPLETED";
		public static const R_LOW_LINEAR_PONTUATION:String		= "R_LOW_LINEAR_PONTUATION";
		
		/**
		 * Create a track object and inserts it into the array
		 * @param	trackItem
		 * @param	trackIndex
		 * @param	trackArray
		 */
		private function createTrack(trackItem:Object, trackIndex:uint, trackArray:Array):void
		{
			trackArray[trackIndex] = new BeatGameTrack(trackIndex);
			trackArray[trackIndex].engine = this;
		}
		
		
		//_______________________________________________________________________________________
		//                                                                             attributes
		
		// the date objeto used to store the initial time
		protected var _dateStarted:Date;
		
		// date objet used to calculete the elapsed time in the game
		protected var _currentDate:Date;
		
		// main game timer
		protected var _timer:Timer;
		
		// current song
		protected var _song:BeatGameSong;
		
		// song notes in time offset ( notes in the board )
		protected var _notesInTimeOffset:Array /*of BeatGameNote*/;
		
		// time offset for delete a note
		protected var _notesTimeOffset:Number;
		
		// time offset for the espected hit time
		protected var _noteHitOffset:Number;
		
		// time range to count a note hit
		protected var _noteHitTimeRange:Number;
		
		// the current beat position ( last beat created )
		protected var _currentNoteIndex:int;
		
		// next beat in stack ( to increase performace ) in the interations
		protected var _nextNote:BeatGameNote;
		
		// number of tracks
		protected var _numTracks:uint;
		
		// track array
		protected var _tracks:Array;
		
		// maximum consecutive notes hit
		protected var _maxCombo:int;
		
		// current number of notes hit consecutively
		protected var _currentCombo:int;
		
		// number of notes sucessiffuly hit
		protected var _hitNotesCount:int;
		
		// number of notes that wasn't hit
		protected var _missedNotesCount:int;
		
		// sum of all absolute deltas of the hit notes
		protected var _totalHitNotesHitDelta:Number;
		
		// the maximum valou _linearPontuation can reach
		protected var _maxLinearPontuation:int;
		
		// the minimum _linearPontuation before lose the game
		private var _minLinearPontuation:int;
		
		// linear pontuation ( hit + 1 , miss - 1 )
		protected var _linearPontuation:int;
		
		//_______________________________________________________________________________________
		//                                                                                 events
		protected var evGameStart:BeatGameEvent;
		protected var evGameLoop:BeatGameEvent;
		protected var evGameOver:BeatGameEvent;
		
		protected var evNoteEnterOffset:BeatGameEvent;
		protected var evNoteLeaveOffset:BeatGameEvent;
		protected var evNoteHit:BeatGameEvent;
		protected var evNoteMiss:BeatGameEvent;
		protected var evNoteLost:BeatGameEvent;
		
		protected var evCurrentComboIncreased:BeatGameEvent;
		protected var evNewMaxCombo:BeatGameEvent;
		protected var evUpdateLinearPontuation:BeatGameEvent;
		
		protected var evTrackHold:BeatGameEvent;
		protected var evTrackRelease:BeatGameEvent;
		
		
		//TODO : criar suporte para notas longas
		/**
		 * Constructor of the engine
		 * @param	tracks nunber of tracks the engine will espect song to have
		 * @param	updateRate the aproximated update rate of engine
		 */
		public function BeatGameEngine(tracks:uint = 4, updateRate:uint = 30) 
		{
			super( this );
			
			_timer = new Timer(1000 / updateRate);
			_timer.addEventListener( TimerEvent.TIMER, gameLoop );
			
			_notesInTimeOffset = new Array();
			
			_tracks = new Array(_numTracks = tracks);
			_tracks.forEach( createTrack );
			
			_notesTimeOffset = 2.4;
			_noteHitOffset = 1.9;
			_noteHitTimeRange = .20;
			_maxLinearPontuation = 10;
			_minLinearPontuation = -10;
			
			_maxCombo = 0;
			_currentCombo = 0;
			
			evGameStart					= createBeatGameEvent(BeatGameEvent.GAME_START);
			evGameLoop					= createBeatGameEvent(BeatGameEvent.GAME_LOOP);
			evGameOver					= createBeatGameEvent(BeatGameEvent.GAME_OVER);
			
			evNoteEnterOffset			= createBeatGameEvent( BeatGameEvent.NOTE_ENTERED_OFFSET );
			evNoteLeaveOffset			= createBeatGameEvent( BeatGameEvent.NOTE_LEAVED_OFFSET );
			evNoteHit					= createBeatGameEvent( BeatGameEvent.NOTE_HIT );
			evNoteMiss					= createBeatGameEvent( BeatGameEvent.NOTE_MISS );
			evNoteLost					= createBeatGameEvent( BeatGameEvent.NOTE_LOST );
			
			evCurrentComboIncreased		= createBeatGameEvent( BeatGameEvent.CURRENT_COMBO_INCREASED );
			evNewMaxCombo				= createBeatGameEvent( BeatGameEvent.NEW_MAX_COMBO );
			evUpdateLinearPontuation	= createBeatGameEvent( BeatGameEvent.UPDATE_LINEAR_PONTUATION );
			
			evTrackHold					= createBeatGameEvent( BeatGameEvent.TRACK_HOLD );
			evTrackRelease				= createBeatGameEvent( BeatGameEvent.TRACK_RELEASE );
		}
		/**
		 * A helper function to create the event template objects
		 * @param	type
		 * @return
		 */
		private function createBeatGameEvent(type:String):BeatGameEvent
		{
			var e:BeatGameEvent = new BeatGameEvent( type );
			e.engine = this;
			return e;
		}
		
		/**
		 * Initialize the engine proccess
		 * @param	song If set the engine will replace the current song value with the passed song
		 * 
		 * @example A simple engine initialization
		 * <listing version="3.0">
		 *  var mySong:BeatGameSong = new MyExtendedSong();
		 *  var engine:BeatGameEngone = new BeatGameEngone();
		 *  
		 *  // ... define events listeners here
		 *  
		 *  engine.gameStart( mySong );
		 * </listing>
		 */
		public function gameStart(song:BeatGameSong = null):void 
		{
			if ( song != null ) this.song = song;
			var i:Number, n:Number;
			
			// limpando o offset
			while ( _notesInTimeOffset.length ) _notesInTimeOffset.pop();
			
			// resetando as tracks
			i = 0; n = _tracks.length;
			for (; i < n; i++)
				getTrack(i).removeAllNotes();
			
			_dateStarted = new Date();
			_nextNote = song.notes[0];
			//_currentBeatPosition = -1;
			_currentNoteIndex = -1;
			
			_maxCombo = 0;
			_currentCombo = 0;
			_hitNotesCount = 0;
			_missedNotesCount = 0;
			_totalHitNotesHitDelta = 0;
			_linearPontuation = 0;
			
			_timer.start();
			
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			evGameStart.song = song;
			dispatchEvent( evGameStart.clone() );
		}
		
		/**
		 * Finish the cuurent song play
		 * @param	reason the reason because the game ended
		 */
		public function gameOver(reason:String):void 
		{
			_timer.stop();
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			evGameOver.reason = reason;
			dispatchEvent( evGameOver.clone() );
		}
		
		/**
		 * Main Game Loop Method
		 * @param	e timerEvent object
		 */
		protected function gameLoop(e:TimerEvent):void 
		{
			var i:uint, n:uint, note:BeatGameNote, track:BeatGameTrack;
			
			//performace upgrade
			var currentTime:Number = this.currentTime; // cache the calculated value
			var currentNotesTimeOffset:Number = currentTime - _notesTimeOffset; // calculate this here before the loop
			
			// enquanto a musica nao terminou
			if ( currentTime < song.length )
			{
				// se a proxima nota estiver no intervalo de tempo atual
				if ( _nextNote && _nextNote.time <= currentTime )
				{
					// corremos as notas ate elas passarem o tempo atual... ( colocando essas notas no offset )
					// isso server para notas que acontecem ao "mesmo tempo"
					for (i = _currentNoteIndex; song.notes[i] && song.notes[i].time <= currentTime; i++)
					{
						note = song.notes[i];
						_notesInTimeOffset.push( note );
						getTrack(note.track).addNote( note );
						//_______________________________________________________________________________________
						//                                                                         dispatch event
						evNoteEnterOffset.note = note;
						dispatchEvent( evNoteEnterOffset.clone() );
					}
					// atualizamos as variaveis de indice e a proximaNota para verificação
					_currentNoteIndex = i;
					_nextNote = song.notes[i] ? song.notes[i] : null;
				}
				
				// se existir alguma nota no offset verificamos se elas ainda estao no offset caso contrario retiramos elas do offset
				if ( _notesInTimeOffset.length > 0 )
				{
					i = 0;
					n = _notesInTimeOffset.length;
					for (; i < n; i++ )
					{	
						if ( _notesInTimeOffset[i].time + _notesInTimeOffset[i].duration < currentNotesTimeOffset )
						{
							note = _notesInTimeOffset.splice(i, 1)[0];
							getTrack( note.track ).removeNote( note );
							
							// jah que retiramos uma note temos que atualizar o 'i' e o 'n' para o for
							i--; n = _notesInTimeOffset.length;
							//_______________________________________________________________________________________
							//                                                                         dispatch event
							evNoteLeaveOffset.note = note;
							dispatchEvent( evNoteLeaveOffset.clone() );
							
							// se note.HitTime é NaN então nossa nota nao vou acertada ( que pena, espero q não tenho um super combo prestes a ser perdido aqui hahaha )
							if ( isNaN( note.hitTime ) )
							{
								_missedNotesCount++;
								increaseLinearPontuation( -1);
							}
							else
							{
								//whooray o cara acertou essa nota.... 
								_hitNotesCount++;
								_totalHitNotesHitDelta += note.hitTimeDelta;
							}
						}
					}
				}
				//_______________________________________________________________________________________
				//                                                                         dispatch event
				dispatchEvent( evGameLoop.clone() );
			}
			else
			{
				gameOver( R_SONG_COMPLETED );
			}
		}
		
		/**
		 * Hold a track and check if a note was hit in the process
		 * @param	trackIndex index of the track to be holded
		 */
		public function holdTrack(trackIndex:int):void 
		{
			// TODO: Mudar actions the controle de acerto para um novo metodo, e deixar a holdTrack somente com os actions referentes a ela
			var track:BeatGameTrack = getTrack(trackIndex);
			if ( track.holding ) return;
			
			var linearPontuationIncrement:int = 0;
			var note:BeatGameNote = getTrackNearestHitNote(track);
			
			if ( note && note.absoluteHitAreaOffset < _noteHitTimeRange )
			{
				note.hitTime = currentHitTime;
				
				track.removeNote( note );
				
				increaseCombo();
				
				linearPontuationIncrement = +1;
				//_______________________________________________________________________________________
				//                                                                         dispatch event
				evNoteHit.note = note;
				evNoteHit.hitTimeDelta = note.hitTimeDelta;
				evNoteHit.track = track;
				dispatchEvent( evNoteHit.clone() );
			}
			else
			{
				resetCurrentCombo();
				linearPontuationIncrement = -1;
				//_______________________________________________________________________________________
				//                                                                         dispatch event
				evNoteMiss.note = note;
				evNoteMiss.track = track;
				dispatchEvent( evNoteMiss.clone() );
			}
			track.holding = true;
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			evTrackHold.track = track;
			dispatchEvent( evTrackHold.clone());
			
			increaseLinearPontuation(linearPontuationIncrement);
		}
		
		/**
		 * releases a track that was holded
		 * @param	trackIndex index of the track to be released
		 */
		public function releaseTrack(trackIndex:uint):void 
		{
			var track:BeatGameTrack = getTrack(trackIndex);
			if ( !track.holding ) return;
			
			track.holding = false;
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			evTrackHold.track = track;
			dispatchEvent( evTrackRelease.clone());
		}
		
		/**
		 * Updates the linear pontuation value and tells everyone about that =]
		 * @param	amount
		 */
		public function increaseLinearPontuation(amount:int):void 
		{
			if ( amount == 0 ) return;
			
			var linearPontuation:int = _linearPontuation + amount;
			if ( linearPontuation > _maxLinearPontuation )
			{
				linearPontuation = _maxLinearPontuation;
			}
			
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			dispatchEvent( evUpdateLinearPontuation.clone());
			
			if ( linearPontuation < _minLinearPontuation)
			{
				gameOver( R_LOW_LINEAR_PONTUATION );
			}
			_linearPontuation = linearPontuation;
		}
		
		/**
		 * update de combo score and if it is greater than the maxCombo updates it to 
		 * @param	amount the amount to increase the current combo
		 */
		public function increaseCombo( amount:int = 1 ):void
		{
			if ( amount == 0 ) return;
			
			_currentCombo += amount;
			//_______________________________________________________________________________________
			//                                                                         dispatch event
			//TODO: adicionar atributos no event para maxCombo e currentCombo
			dispatchEvent( evCurrentComboIncreased.clone() );
			
			if ( _currentCombo > _maxCombo )
			{
				_maxCombo = _currentCombo;
				//_______________________________________________________________________________________
				//                                                                         dispatch event
				//TODO: adicionar atributos no event para maxCombo e currentCombo
				dispatchEvent( evNewMaxCombo.clone() );
			}
		}
		
		/**
		 * resets the current combo score
		 */
		public function resetCurrentCombo():void 
		{
			_currentCombo = 0;
		}
		
		/**
		 * returns the track object at the trackIndex
		 * @param	track
		 * @return
		 */
		public function  getTrack(trackIndex:uint):BeatGameTrack 
		{
			return _tracks[trackIndex] as BeatGameTrack;
		}
		
		/**
		 * returns the nearest note to hit area
		 * @param	track
		 * @return
		 */
		public function getTrackNearestHitNote(track:BeatGameTrack):BeatGameNote
		{
			var nearestNote:BeatGameNote = track.getNote(0);
			var i:int = 1, n:int = track.notes.length;
			
			while ( i < n )
			{
				if ( track.getNote(i).absoluteHitAreaOffset < nearestNote.absoluteHitAreaOffset )
					nearestNote = track.getNote(i);
				else
					break;
				i++;
			}
			return nearestNote;
		}
		
		/**
		 * calculates the current time elapsed from the start of the game ( very acurated )
		 */
		public function get currentTime():Number
		{
			if ( !_dateStarted ) return Number.NaN;
			return (( new Date() ).getTime() - _dateStarted.getTime() ) * .001;
		}
		
		/**
		 * calculates the current hitTime ( currentTime - hitnoteOffset )
		 */
		public function get currentHitTime():Number { return currentTime - _noteHitOffset; }
		
		/**
		 * Accessor for the local song object
		 */
		public function get song():BeatGameSong { return _song; }
		public function set song(value:BeatGameSong):void
		{
			if ( _song )
			{
				_song.engine = null;
			}
			_song = value;
			if ( _song )
			{
				_song.engine = this;
			}
		}
		
		/**
		 * Accessor for the notesTimeOffset attribute
		 */
		public function get notesTimeOffset():Number { return _notesTimeOffset; }
		
		public function get noteHitOffset():Number { return _noteHitOffset; }
		
		public function get noteHitTimeRange():Number { return _noteHitTimeRange; }
		
		public function get maxCombo():int { return _maxCombo; }
		
		public function get currentCombo():int { return _currentCombo; }
		
		public function get missedNotesCount():int { return _missedNotesCount; }
		
		public function get hitNotesCount():int { return _hitNotesCount; }
		
		public function get totalHitNotesHitDelta():Number { return _totalHitNotesHitDelta; }
		
		public function get linearPontuation():int { return _linearPontuation; }
		
	}
	
}